package com.pucong.gitee.database.backup.business.service.util.cmdOs;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jcraft.jsch.*;
import com.pucong.gitee.database.backup.business.service.cover.ComputerServiceBeanConvert;
import com.pucong.gitee.database.backup.business.service.mapper.ServiceComputerMapper;
import com.pucong.gitee.database.backup.business.service.service.ServiceLinuxLogService;
import com.pucong.gitee.database.backup.business.sys.mapper.SysFileMapper;
import com.pucong.gitee.database.backup.business.sys.mapper.SysUserInfoMapper;
import com.pucong.gitee.database.backup.common.entity.service.SSHConnectInfo;
import com.pucong.gitee.database.backup.common.entity.service.ServiceComputer;
import com.pucong.gitee.database.backup.common.entity.service.dto.ServiceComputerContnectTestDto;
import com.pucong.gitee.database.backup.common.entity.service.dto.ServiceComputerLoginDto;
import com.pucong.gitee.database.backup.common.entity.service.dto.ServiceLinuxLogSaveDto;
import com.pucong.gitee.database.backup.common.entity.sys.UserJwt;
import com.pucong.gitee.database.backup.common.entity.sys.jwt.CheckResult;
import com.pucong.gitee.database.backup.common.entity.websock.LinuxWebsocketMsgDto;
import com.pucong.gitee.database.backup.common.selfenum.ServiceCommandType;
import com.pucong.gitee.database.backup.configuration.exception.BusinessException;
import com.pucong.gitee.database.backup.utils.JwtUtils;
import com.pucong.gitee.database.backup.utils.ThreadPoolUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.*;
import java.net.URLDecoder;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @Description: WebSSH业务逻辑实现
 * @Author: NoCortY
 * @Date: 2020/3/8
 */
@Service
public class WebSSHServiceImpl implements WebSSHService {
    //存放ssh连接信息的map
    private static Map<String, Object> sshMap = new ConcurrentHashMap<>();

    private Logger logger = LoggerFactory.getLogger(WebSSHServiceImpl.class);

    @Autowired
    private ServiceComputerMapper serviceComputerMapper;

    @Autowired
    private ComputerServiceBeanConvert computerServiceBeanConvert;

    @Autowired
    private SysFileMapper sysFileMapper;

    @Autowired
    private SysUserInfoMapper sysUserInfoMapper;

    @Autowired
    private ServiceLinuxLogService serviceLinuxLogService;

    private static byte[] zmodemszstart = new byte[]{42, 42, 24, 66, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 13, -118, 17};
    private static byte[] zmodemend = new byte[]{42, 42, 24, 66, 48, 56, 48, 48, 48, 48, 48, 48, 48, 48, 48, 50, 50, 100, 13};
    private static byte[] zmodemrzstart = new byte[]{13, 10, 114, 122, 32, 119, 97, 105, 116, 105, 110, 103, 32, 116, 111, 32, 114, 101, 99, 101, 105, 118, 101, 46, 42, 42, 24, 66, 48, 49, 48, 48, 48, 48, 48, 48, 50, 51, 98, 101, 53, 48, 13, -118, 17};
    private static byte[] zmodemcancel = new byte[]{24, 24, 24, 24, 24, 8, 8, 8, 8, 8};
    private static byte[] zmodemStop = new byte[]{117, 112, 108, 111, 97, 100, 46, 112, 110, 103, 32};

    // 当前执行的命令
    private static StringBuffer currentCommand = new StringBuffer();
    // 禁止执行的命令
    private static String[] CANT_EXECUTE_COMMAND = new String[]{"rm -rf *", "rm -rf .", "rm -rf /", "rm -f"};

    public static String decodeUTF8Str(String xStr) throws UnsupportedEncodingException {
        return URLDecoder.decode(xStr.replaceAll("\\\\x", "%"), "utf-8");
    }

    public static void main(String[] args) throws UnsupportedEncodingException {
//        zmodemszstart = b'rz\r**\x18B00000000000000\r\x8a'
//        zmodemend = b'**\x18B0800000000022d\r\x8a'
//        zmodemrzstart = b'rz waiting to receive.**\x18B0100000023be50\r\x8a'
//        zmodemcancel = b'\x18\x18\x18\x18\x18\x08\x08\x08\x08\x08'

        String utf8String = "\\x8a";
        String s = decodeUTF8Str(utf8String);
        System.out.println(byteToList(s.getBytes("utf-8")).toString());
        System.out.println(byteToList("OO".getBytes("utf-8")).toString());

        byte[] zmodemStop = new byte[]{91, 114, 111, 111, 116, 64, 86, 77, 45, 48, 45, 49, 55, 45, 99, 101, 110, 116, 111, 115, 32, 126, 93, 35, 32};

        String msgOut = new String(zmodemStop, StandardCharsets.UTF_8);
        System.out.println(msgOut);

        System.out.println(isIncludeByteArray(zmodemStop, zmodemszstart));
    }

    /**
     * @param session
     * @Description: 初始化连接
     * @Param: [session]
     * @return: void
     * @Author: NoCortY
     * @Date: 2020/3/7
     */
    @Override
    public void initConnection(javax.websocket.Session session) {
        JSch jSch = new JSch();
        SSHConnectInfo sshConnectInfo = new SSHConnectInfo();
        sshConnectInfo.setJSch(jSch);
        sshConnectInfo.setSession(session);
        String uuid = session.getId();
        //将这个ssh连接信息放入map中
        sshMap.put(uuid, sshConnectInfo);
    }

    @Override
    public Boolean connectTest(ServiceComputerContnectTestDto dto) {
        try {
            Session session = null;
            Properties config = new Properties();
            config.put("StrictHostKeyChecking", "no");
            //获取jsch的会话
            JSch jSch = new JSch();
            session = jSch.getSession(dto.getUsername(), dto.getHost(), dto.getPort());
            session.setConfig(config);
            //设置密码
            session.setPassword(dto.getPassword());
            //连接  超时时间30s
            session.connect(30000);

            //开启shell通道
            Channel channel = session.openChannel("shell");

            //通道连接 超时时间3s
            channel.connect(3000);

            //转发消息
            transToSSH(channel, "\r");

            channel.disconnect();
            session.disconnect();
        } catch (Exception e) {
            return false;
        }
        return true;
    }

    /**
     * @Description: 处理客户端发送的数据
     * @Param: [buffer, session]
     * @return: void
     * @Author: NoCortY
     * @Date: 2020/3/7
     */
    @Override
    public void recvHandle(String buffer, javax.websocket.Session session) {
        ObjectMapper objectMapper = new ObjectMapper();
        LinuxWebsocketMsgDto webSSHData = null;
        try {
            webSSHData = objectMapper.readValue(buffer, LinuxWebsocketMsgDto.class);
        } catch (IOException e) {
            logger.error("Json转换异常");
            logger.error("异常信息:{}", e);
            throw new BusinessException("传入参数错误");
        }
        ServiceComputer serviceComputer = serviceComputerMapper.selectById(webSSHData.getLinuxId());
        if (serviceComputer == null) {
            logger.error("传入参数错误");
            throw new BusinessException("传入参数错误");
        }
        ServiceComputerLoginDto serviceComputerLoginDto = computerServiceBeanConvert.do2loginDto(serviceComputer);
        String id = session.getId();
        if (webSSHData.getOperate() == ServiceCommandType.连接) {
            // 校验用户权限
            UserJwt userJwt = checkUserPermission(webSSHData.getToken(), webSSHData.getLinuxId());

            // 找到刚才存储的ssh连接对象
            SSHConnectInfo sshConnectInfo = (SSHConnectInfo) sshMap.get(id);
            sshConnectInfo.setServiceId(webSSHData.getLinuxId());
            sshConnectInfo.setUserId(userJwt.getUuid());
            // 启动线程异步处理
            ThreadPoolUtils.executor(() -> {
                try {
                    connectToSSH(sshConnectInfo, serviceComputerLoginDto, session);
                } catch (Exception e) {
                    logger.error("webssh连接异常");
                    logger.error("异常信息:{}", e);
                    throw new BusinessException("webssh连接异常");
                } finally {
                    close(session);
                }
            });
        } else if (webSSHData.getOperate() == ServiceCommandType.执行指令) {
            String command = webSSHData.getCommand();
            SSHConnectInfo sshConnectInfo = (SSHConnectInfo) sshMap.get(id);
            if (sshConnectInfo != null) {
                command = checkCommand(sshConnectInfo, command, session);
                try {
                    transToSSH(sshConnectInfo.getChannel(), command);
                } catch (IOException e) {
                    logger.error("webssh连接异常");
                    logger.error("异常信息:{}", e.getMessage());
                    close(session);
                }
            }
        } else if (webSSHData.getOperate() == ServiceCommandType.上传文件) {
            List<Byte> bytes = webSSHData.getBytes();
            SSHConnectInfo sshConnectInfo = (SSHConnectInfo) sshMap.get(id);
            if (sshConnectInfo != null) {
                try {
                    transToSSH(sshConnectInfo.getChannel(), listToByte(bytes));
                } catch (IOException e) {
                    logger.error("webssh连接异常");
                    logger.error("异常信息:{}", e.getMessage());
                    close(session);
                }
            }
        } else {
            logger.error("不支持的操作");
            close(session);
        }
    }

    /**
     * 校验执行的指令是否可以执行
     *
     * @param sshConnectInfo
     * @param command
     * @param socketSession
     * @return
     */
    private String checkCommand(SSHConnectInfo sshConnectInfo, String command, javax.websocket.Session socketSession) {
        if (command.contains("\r") || command.contains("\n")) {
            currentCommand.append(command);
            String commands = currentCommand.toString();
            commands = commands.replaceAll("\t", "").replaceAll(" +", " ");
            String[] split = commands.split("\r");
            currentCommand = new StringBuffer();
            List<ServiceLinuxLogSaveDto> dtoList = new ArrayList<>();
            for (String s : split) {
                if (StringUtils.isNotBlank(s) && Arrays.asList(CANT_EXECUTE_COMMAND).contains(s)) {
                    sendMessage(sshConnectInfo, socketSession, "\r can't execute the command,it's dangerous!".getBytes(), 0);
                    return "\u0003";
                } else if(StringUtils.isNotBlank(s)){
                    ServiceLinuxLogSaveDto dto = new ServiceLinuxLogSaveDto();
                    dto.setUserId(sshConnectInfo.getUserId());
                    dto.setServiceId(sshConnectInfo.getServiceId());
                    dto.setSessionId(socketSession.getId());
                    dto.setExeCmd(s);
                    dtoList.add(dto);
                }
            }
            if (dtoList.size() > 0) {
                serviceLinuxLogService.addBatchLog(dtoList);
            }
        } else {
            currentCommand.append(command);
        }
        return command;
    }

    private static List<Byte> byteToList(byte[] bytes) {
        int len;
        if (bytes == null || bytes.length == 0) {
            return null;
        }
        len = bytes.length;
        List buf = new ArrayList<Byte>(len);
        for (int j = 0; j < len; j++) {
            buf.add(bytes[j]);
        }
        return buf;
    }

    private byte[] listToByte(List<Byte> list) {
        int len;
        if (list == null || list.isEmpty()) {
            return null;
        }
        len = list.size();
        byte[] buf = new byte[len];
        for (int j = 0; j < len; j++) {
            buf[j] = list.get(j);
        }
        return buf;
    }

    private UserJwt checkUserPermission(String token, String linuxId) {
        if (StringUtils.isBlank(token)) {
            logger.error("用户认证失败");
            throw new BusinessException("用户认证失败");
        }
        ObjectMapper objectMapper = new ObjectMapper();
        CheckResult checkResult = JwtUtils.validateJWT(token);
        if (checkResult.isSuccess()) {
            // 获取用户信息
            String subject = checkResult.getClaims().getSubject();
            UserJwt loginUser = null;
            try {
                loginUser = objectMapper.readValue(subject, UserJwt.class);
            } catch (JsonProcessingException e) {
                throw new BusinessException("反序列化用户数据失败");
            }
            // 校验用户权限
            List<String> userIds = null;
            if (!"superadmin".equals(loginUser.getLoginName())) {
                userIds = sysUserInfoMapper.selectChildrenUserId(loginUser.getUuid());
            }
            LambdaQueryWrapper<ServiceComputer> serviceComputerParams = new LambdaQueryWrapper<>();
            serviceComputerParams.eq(ServiceComputer::getUuid, linuxId);
            serviceComputerParams.in(userIds != null, ServiceComputer::getCreateBy, userIds);
            ServiceComputer serviceComputer = serviceComputerMapper.selectOne(serviceComputerParams);
            if (serviceComputer == null) {
                throw new BusinessException("该用户没有操作该linux的权限");
            }
            return loginUser;
        }else{
            throw new BusinessException("用户未授权，无法访问");
        }
    }

    @Override
    public void sendMessage(SSHConnectInfo sshConnectInfo, javax.websocket.Session session, byte[] buffer, int count) {
        synchronized (session) {
            try {
                if (sshConnectInfo.getZmodemOO()) {
                    sshConnectInfo.setZmodemOO(false);
                    if (buffer.length == 0) {
                        return;
                    }
                    if (Arrays.equals(buffer, zmodemStop)) {
                        session.getBasicRemote().sendBinary(ByteBuffer.wrap(buffer));
                        return;
                    }
                }
                if (sshConnectInfo.getZmodemStart()) {
                    if (isIncludeByteArray(buffer, zmodemend)) {
                        sshConnectInfo.setZmodemStart(false);
                        sshConnectInfo.setZmodemOO(true);
                    } else if (isIncludeByteArray(buffer, zmodemcancel)) {
                        sshConnectInfo.setZmodemStart(false);
                        session.getBasicRemote().sendText("\n");
                    }
                    session.getBasicRemote().sendBinary(ByteBuffer.wrap(buffer));
                    return;
                } else {
                    if (isIncludeByteArray(buffer, zmodemszstart) || isIncludeByteArray(buffer, zmodemrzstart)) {
                        sshConnectInfo.setZmodemStart(true);
                        session.getBasicRemote().sendBinary(ByteBuffer.wrap(buffer));
                        return;
                    }
                }
                final String msgOut = new String(buffer, StandardCharsets.UTF_8);
                session.getBasicRemote().sendText(msgOut);
            } catch (IOException e) {
                logger.error("发送消息失败，5次失败后停止发送，失败次数：" + count, e);
                if (count < 5) {
                    this.sendMessage(sshConnectInfo, session, buffer, count++);
                }
            }
        }
    }

    /**
     * 判断是否一个字节数组按顺序包含另一个字节数组
     *
     * @param pSrcByteArray
     * @param pSubByteArray
     * @return
     */
    public static boolean isIncludeByteArray(byte[] pSrcByteArray, byte[] pSubByteArray) {
        boolean retValue = false;
        int lvSrcByteArrayLen = pSrcByteArray.length;
        int lvSubByteArrayLen = pSubByteArray.length;

        while (true) {
            if (lvSrcByteArrayLen < lvSubByteArrayLen) {
                break;
            }
            int lvHitByteNumberValue = 0;
            for (int i = 0; i < lvSrcByteArrayLen; i++) {
                int tvByteValue = pSrcByteArray[i];
                if (lvHitByteNumberValue == pSubByteArray.length) {
                    retValue = true;
                    break;
                }
                if (tvByteValue == pSubByteArray[lvHitByteNumberValue]) {
                    lvHitByteNumberValue++;
                    continue;
                }
                lvHitByteNumberValue = 0;
                //剩余字节数
                int tvRemaindingByteLen = lvSrcByteArrayLen - i - 1;
                if (tvRemaindingByteLen < pSubByteArray.length) {
                    break;
                }
            }
            if (lvHitByteNumberValue == lvSubByteArrayLen) {
                retValue = true;
            }
            break;
        }
        return retValue;
    }

    @Override
    public void close(javax.websocket.Session session) {
        String id = session.getId();
        SSHConnectInfo sshConnectInfo = (SSHConnectInfo) sshMap.get(id);
        if (sshConnectInfo != null) {
            //断开连接
            if (sshConnectInfo.getChannel() != null) {
                sshConnectInfo.getChannel().disconnect();
            }
            //map中移除
            sshMap.remove(id);
        }
    }

    /**
     * @Description: 使用jsch连接终端
     * @Param: [cloudSSH, webSSHData, webSocketSession]
     * @return: void
     * @Author: NoCortY
     * @Date: 2020/3/7
     */
    private void connectToSSH(SSHConnectInfo sshConnectInfo, ServiceComputerLoginDto dto, javax.websocket.Session socketSession) throws JSchException, IOException {
        Session session = null;
        Properties config = new Properties();
        config.put("StrictHostKeyChecking", "no");
        //获取jsch的会话
        session = sshConnectInfo.getJSch().getSession(dto.getUsername(), dto.getHost(), dto.getPort());
        session.setConfig(config);
        //设置密码
        session.setPassword(dto.getPassword());
        //连接  超时时间30s
        session.connect(30000);

        //开启shell通道
        Channel channel = session.openChannel("shell");

        //通道连接 超时时间3s
        channel.connect(3000);

        //设置channel
        sshConnectInfo.setChannel(channel);

        //转发消息
        transToSSH(channel, "\r");

        //读取终端返回的信息流
        InputStream inputStream = channel.getInputStream();
        try {
            //循环读取
            byte[] buffer = new byte[1024];
            int i = 0;
            //如果没有数据来，线程会一直阻塞在这个地方等待数据。
            while ((i = inputStream.read(buffer)) != -1) {
                sendMessage(sshConnectInfo, socketSession, Arrays.copyOfRange(buffer, 0, i), 1);
            }

        } finally {
            //断开连接后关闭会话
            session.disconnect();
            channel.disconnect();
            if (inputStream != null) {
                inputStream.close();
            }
        }
    }

    /**
     * @Description: 将消息转发到终端
     * @Param: [channel, data]
     * @return: void
     * @Author: NoCortY
     * @Date: 2020/3/7
     */
    private void transToSSH(Channel channel, byte[] bytes) throws IOException {
        if (channel != null) {
            OutputStream outputStream = channel.getOutputStream();
            outputStream.write(bytes);
            outputStream.flush();
        }
    }

    private void transToSSH(Channel channel, String command) throws IOException {
        transToSSH(channel, command.getBytes());
    }

    /**
     * 上传文件
     *
     * @param session
     * @param srcPath
     * @param dst
     * @param monitor
     * @throws Exception
     */
    public void uploadFilesToServer(Session session, String srcPath, String dst, SftpProgressMonitor monitor) throws Exception {
        ChannelSftp sftp = upload(session, srcPath, dst, monitor);
        if (sftp != null) {
            sftp.quit();
            sftp.disconnect();
        }
        ChannelSftpSingleton.getInstance().closeChannel();
    }

    /**
     * 递归上传文件
     *
     * @param path    文件目录
     * @param dst     目标目录
     * @param monitor
     * @return
     * @throws SftpException
     */
    private ChannelSftp upload(Session session, String path, String dst, SftpProgressMonitor monitor) throws SftpException, JSchException {
        File file = new File(path);
        if (!file.exists()) {
            return null;
        }
        ChannelSftp chSftp = getChannelSftp(session);

        if (chSftp == null) {
            return null;
        }
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            if (files == null || files.length <= 0) {
                return null;
            }
            for (File f : files) {
                String fp = f.getAbsolutePath();
                if (f.isDirectory()) {
                    String mkdir = dst + "/" + f.getName();
                    try {
                        chSftp.cd(mkdir);
                    } catch (Exception e) {
                        chSftp.mkdir(mkdir);
                    }
                    upload(session, fp, mkdir, monitor);
                } else {
                    chSftp.put(fp, dst, monitor, ChannelSftp.OVERWRITE);
                }
            }
        } else {
            String fp = file.getAbsolutePath();
            chSftp.put(fp, dst, monitor, ChannelSftp.OVERWRITE);
        }
        return chSftp;
    }

    /**
     * 获得SFTP Channel
     *
     * @return ChannelSftp Instance
     * @throws JSchException
     */
    private ChannelSftp getChannelSftp(Session session) throws JSchException {
        // 打开SFTP通道
        Channel channel = session.openChannel("sftp");
        // 建立SFTP通道的连接
        channel.connect();
        return (ChannelSftp) channel;
    }
}
